#!/usr/bin/env python3
"""
CodeQL扫描结果分析器
用于分析SARIF文件，生成安全漏洞统计和趋势分析
"""

import os
import json
import argparse
import logging
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Tuple
from collections import defaultdict, Counter
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns


class CodeQLResultAnalyzer:
    """CodeQL扫描结果分析器"""
    
    def __init__(self, results_dir: str):
        """初始化分析器"""
        self.results_dir = Path(results_dir)
        self.setup_logging()
        
        # 安全规则分类
        self.rule_categories = {
            'sql-injection': 'SQL注入',
            'xss': '跨站脚本攻击',
            'path-traversal': '路径遍历',
            'command-injection': '命令注入',
            'deserialization': '反序列化漏洞',
            'crypto': '加密问题',
            'authentication': '认证问题',
            'authorization': '授权问题',
            'input-validation': '输入验证',
            'error-handling': 'errorProcessing',
            'memory-safety': '内存安全',
            'concurrency': '并发问题',
            'performance': '性能问题',
            'code-quality': '代码质量',
            'other': '其他'
        }
        
        # 严重程度映射
        self.severity_mapping = {
            'error': '高',
            'warning': '中',
            'note': '低',
            'info': '信息'
        }
    
    def setup_logging(self):
        """设置日志"""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s'
        )
        self.logger = logging.getLogger(__name__)
    
    def load_sarif_files(self) -> List[Dict]:
        """加载所有SARIF文件"""
        sarif_files = []
        
        for sarif_file in self.results_dir.rglob('*.sarif'):
            try:
                with open(sarif_file, 'r', encoding='utf-8') as f:
                    sarif_data = json.load(f)
                    sarif_data['_file_path'] = str(sarif_file)
                    sarif_data['_language'] = sarif_file.parent.name
                    sarif_data['_project_name'] = sarif_file.stem
                    sarif_files.append(sarif_data)
            except Exception as e:
                self.logger.error(f"加载SARIF文件failed: {sarif_file}, error: {e}")
        
        self.logger.info(f"加载了 {len(sarif_files)} projectsSARIF文件")
        return sarif_files
    
    def extract_findings(self, sarif_files: List[Dict]) -> List[Dict]:
        """提取所有Found的问题"""
        findings = []
        
        for sarif_data in sarif_files:
            runs = sarif_data.get('runs', [])
            
            for run in runs:
                results = run.get('results', [])
                
                for result in results:
                    finding = {
                        'project_name': sarif_data['_project_name'],
                        'language': sarif_data['_language'],
                        'file_path': sarif_data['_file_path'],
                        'rule_id': result.get('ruleId', 'unknown'),
                        'message': result.get('message', {}).get('text', ''),
                        'severity': self._get_severity(result),
                        'category': self._categorize_rule(result.get('ruleId', '')),
                        'locations': result.get('locations', []),
                        'properties': result.get('properties', {}),
                        'timestamp': datetime.now().isoformat()
                    }
                    findings.append(finding)
        
        self.logger.info(f"提取了 {len(findings)} projects安全问题")
        return findings
    
    def _get_severity(self, result: Dict) -> str:
        """获取问题严重程度"""
        # 从不同位置获取严重程度
        severity = result.get('level', '')
        if not severity:
            severity = result.get('properties', {}).get('severity', '')
        if not severity:
            severity = result.get('message', {}).get('severity', '')
        
        return self.severity_mapping.get(severity.lower(), '未知')
    
    def _categorize_rule(self, rule_id: str) -> str:
        """根据规则ID分类问题"""
        rule_id_lower = rule_id.lower()
        
        for category, _ in self.rule_categories.items():
            if category in rule_id_lower:
                return self.rule_categories[category]
        
        return '其他'
    
    def generate_statistics(self, findings: List[Dict]) -> Dict:
        """生成统计信息"""
        stats = {
            'total_findings': len(findings),
            'by_language': defaultdict(int),
            'by_severity': defaultdict(int),
            'by_category': defaultdict(int),
            'by_project': defaultdict(int),
            'top_rules': Counter(),
            'top_projects': Counter(),
            'languages': set(),
            'projects': set()
        }
        
        for finding in findings:
            # 按Language统计
            stats['by_language'][finding['language']] += 1
            stats['languages'].add(finding['language'])
            
            # 按严重程度统计
            stats['by_severity'][finding['severity']] += 1
            
            # 按类别统计
            stats['by_category'][finding['category']] += 1
            
            # 按项目统计
            stats['by_project'][finding['project_name']] += 1
            stats['projects'].add(finding['project_name'])
            
            # 规则统计
            stats['top_rules'][finding['rule_id']] += 1
            
            # 项目统计
            stats['top_projects'][finding['project_name']] += 1
        
        # 转换为普通字典
        stats['by_language'] = dict(stats['by_language'])
        stats['by_severity'] = dict(stats['by_severity'])
        stats['by_category'] = dict(stats['by_category'])
        stats['by_project'] = dict(stats['by_project'])
        stats['languages'] = list(stats['languages'])
        stats['projects'] = list(stats['projects'])
        
        return stats
    
    def generate_reports(self, findings: List[Dict], stats: Dict, output_dir: str):
        """生成Analysis reports"""
        output_dir = Path(output_dir)
        output_dir.mkdir(parents=True, exist_ok=True)
        
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        
        # Generate JSON report
        self._generate_json_report(findings, stats, output_dir / f'analysis_report_{timestamp}.json')
        
        # Generate HTML report
        self._generate_html_report(findings, stats, output_dir / f'analysis_report_{timestamp}.html')
        
        # Generate CSV report
        self._generate_csv_report(findings, output_dir / f'findings_{timestamp}.csv')
        
        # Generate charts
        self._generate_charts(stats, output_dir / f'charts_{timestamp}')
        
        # Generate summary report
        self._generate_summary_report(stats, output_dir / f'summary_{timestamp}.txt')
    
    def _generate_json_report(self, findings: List[Dict], stats: Dict, output_file: Path):
        """Generate JSON report"""
        report = {
            'analysis_info': {
                'timestamp': datetime.now().isoformat(),
                'total_findings': len(findings),
                'total_projects': len(stats['projects']),
                'total_languages': len(stats['languages'])
            },
            'statistics': stats,
            'findings': findings
        }
        
        with open(output_file, 'w', encoding='utf-8') as f:
            json.dump(report, f, indent=2, ensure_ascii=False)
        
        self.logger.info(f"JSON report generated: {output_file}")
    
    def _generate_html_report(self, findings: List[Dict], stats: Dict, output_file: Path):
        """Generate HTML report"""
        html_content = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CodeQL安全Analysis reports</title>
    <style>
        body {{ font-family: Arial, sans-serif; margin: 20px; }}
        .header {{ background-color: #f0f0f0; padding: 20px; border-radius: 5px; }}
        .stats {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0; }}
        .stat-card {{ background-color: #e8f4fd; padding: 15px; border-radius: 5px; text-align: center; }}
        .chart {{ margin: 30px 0; }}
        .findings-table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
        .findings-table th, .findings-table td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
        .findings-table th {{ background-color: #f2f2f2; }}
        .severity-high {{ color: red; font-weight: bold; }}
        .severity-medium {{ color: orange; font-weight: bold; }}
        .severity-low {{ color: green; font-weight: bold; }}
    </style>
</head>
<body>
    <div class="header">
        <h1>CodeQL安全Analysis reports</h1>
        <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
        <p>总Found问题: {stats['total_findings']} projects</p>
        <p>涉及项目: {len(stats['projects'])} projects</p>
        <p>涉及Language: {len(stats['languages'])} 种</p>
    </div>
    
    <div class="stats">
        <div class="stat-card">
            <h3>按严重程度</h3>
            <p>高: {stats['by_severity'].get('高', 0)}</p>
            <p>中: {stats['by_severity'].get('中', 0)}</p>
            <p>低: {stats['by_severity'].get('低', 0)}</p>
        </div>
        <div class="stat-card">
            <h3>按Language</h3>
            {self._generate_language_stats_html(stats['by_language'])}
        </div>
        <div class="stat-card">
            <h3>按类别</h3>
            {self._generate_category_stats_html(stats['by_category'])}
        </div>
    </div>
    
    <h2>详细Found</h2>
    <table class="findings-table">
        <thead>
            <tr>
                <th>项目</th>
                <th>Language</th>
                <th>规则ID</th>
                <th>严重程度</th>
                <th>类别</th>
                <th>描述</th>
            </tr>
        </thead>
        <tbody>
"""
        
        for finding in findings[:100]:  # 只显示前100projects
            severity_class = f"severity-{finding['severity'].lower()}"
            html_content += f"""
            <tr>
                <td>{finding['project_name']}</td>
                <td>{finding['language']}</td>
                <td>{finding['rule_id']}</td>
                <td class="{severity_class}">{finding['severity']}</td>
                <td>{finding['category']}</td>
                <td>{finding['message'][:100]}...</td>
            </tr>
"""
        
        html_content += """
        </tbody>
    </table>
</body>
</html>
"""
        
        with open(output_file, 'w', encoding='utf-8') as f:
            f.write(html_content)
        
        self.logger.info(f"HTML report generated: {output_file}")
    
    def _generate_language_stats_html(self, by_language: Dict) -> str:
        """生成Language统计HTML"""
        html = ""
        for lang, count in sorted(by_language.items(), key=lambda x: x[1], reverse=True)[:5]:
            html += f"<p>{lang}: {count}</p>"
        return html
    
    def _generate_category_stats_html(self, by_category: Dict) -> str:
        """生成类别统计HTML"""
        html = ""
        for category, count in sorted(by_category.items(), key=lambda x: x[1], reverse=True)[:5]:
            html += f"<p>{category}: {count}</p>"
        return html
    
    def _generate_csv_report(self, findings: List[Dict], output_file: Path):
        """Generate CSV report"""
        df = pd.DataFrame(findings)
        df.to_csv(output_file, index=False, encoding='utf-8')
        self.logger.info(f"CSV报告已生成: {output_file}")
    
    def _generate_charts(self, stats: Dict, output_dir: Path):
        """Generate charts"""
        output_dir.mkdir(parents=True, exist_ok=True)
        
        # Set Chinese font
        plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
        plt.rcParams['axes.unicode_minus'] = False
        
        # Language statistics chart
        if stats['by_language']:
            plt.figure(figsize=(12, 6))
            languages = list(stats['by_language'].keys())
            counts = list(stats['by_language'].values())
            plt.bar(languages, counts)
            plt.title('按Language统计安全问题')
            plt.xlabel('编程Language')
            plt.ylabel('问题数量')
            plt.xticks(rotation=45)
            plt.tight_layout()
            plt.savefig(output_dir / 'by_language.png', dpi=300, bbox_inches='tight')
            plt.close()
        
        # Severity statistics chart
        if stats['by_severity']:
            plt.figure(figsize=(8, 6))
            severities = list(stats['by_severity'].keys())
            counts = list(stats['by_severity'].values())
            colors = ['red' if s == '高' else 'orange' if s == '中' else 'green' for s in severities]
            plt.pie(counts, labels=severities, colors=colors, autopct='%1.1f%%')
            plt.title('按严重程度统计')
            plt.savefig(output_dir / 'by_severity.png', dpi=300, bbox_inches='tight')
            plt.close()
        
        # Category statistics chart
        if stats['by_category']:
            plt.figure(figsize=(12, 6))
            categories = list(stats['by_category'].keys())
            counts = list(stats['by_category'].values())
            plt.bar(categories, counts)
            plt.title('按类别统计安全问题')
            plt.xlabel('问题类别')
            plt.ylabel('问题数量')
            plt.xticks(rotation=45)
            plt.tight_layout()
            plt.savefig(output_dir / 'by_category.png', dpi=300, bbox_inches='tight')
            plt.close()
        
        self.logger.info(f"Charts generated: {output_dir}")
    
    def _generate_summary_report(self, stats: Dict, output_file: Path):
        """Generate summary report"""
        with open(output_file, 'w', encoding='utf-8') as f:
            f.write("CodeQL安全分析汇总报告\n")
            f.write(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            f.write(f"{'='*50}\n\n")
            
            f.write(f"总体统计:\n")
            f.write(f"  总Found问题: {stats['total_findings']}\n")
            f.write(f"  涉及项目: {len(stats['projects'])}\n")
            f.write(f"  涉及Language: {len(stats['languages'])}\n\n")
            
            f.write(f"按严重程度统计:\n")
            for severity, count in sorted(stats['by_severity'].items(), key=lambda x: x[1], reverse=True):
                f.write(f"  {severity}: {count}\n")
            
            f.write(f"\n按Language统计:\n")
            for language, count in sorted(stats['by_language'].items(), key=lambda x: x[1], reverse=True):
                f.write(f"  {language}: {count}\n")
            
            f.write(f"\n按类别统计:\n")
            for category, count in sorted(stats['by_category'].items(), key=lambda x: x[1], reverse=True):
                f.write(f"  {category}: {count}\n")
            
            f.write(f"\n问题最多的项目:\n")
            for project, count in stats['top_projects'].most_common(10):
                f.write(f"  {project}: {count}\n")
            
            f.write(f"\n最常见的规则:\n")
            for rule, count in stats['top_rules'].most_common(10):
                f.write(f"  {rule}: {count}\n")
        
        self.logger.info(f"Summary report generated: {output_file}")
    
    def analyze(self, output_dir: str = None):
        """执行完整分析"""
        if output_dir is None:
            output_dir = self.results_dir.parent / 'analysis'
        
        self.logger.info("Starting analysisCodeQL扫描结果")
        
        # 加载SARIF文件
        sarif_files = self.load_sarif_files()
        if not sarif_files:
            self.logger.warning("未FoundSARIF文件")
            return
        
        # 提取Found的问题
        findings = self.extract_findings(sarif_files)
        if not findings:
            self.logger.warning("未Found任何安全问题")
            return
        
        # 生成统计信息
        stats = self.generate_statistics(findings)
        
        # Generate reports
        self.generate_reports(findings, stats, output_dir)
        
        self.logger.info("Analysis completed")


def main():
    """Main function"""
    parser = argparse.ArgumentParser(description='CodeQL扫描结果分析器')
    parser.add_argument('--results-dir', required=True, help='结果目录路径')
    parser.add_argument('--output-dir', help='输出目录路径')
    
    args = parser.parse_args()
    
    analyzer = CodeQLResultAnalyzer(args.results_dir)
    analyzer.analyze(args.output_dir)


if __name__ == "__main__":
    main()
